﻿using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace LittleBee.JsonRpc
{
	public enum JsonRpcResponseErrorType
	{
		ParseError,
		InvalidResponse,
		InvalidSequenceID,
		NotFoundSequenceID,
		ProcessError
	}

	public class JsonRpcExceptionEventArg : EventArgs
	{
		private JObject _joResp;
		private JsonRpcResponseErrorType _nErrorType;
		public JsonRpcExceptionEventArg(JObject joResp, JsonRpcResponseErrorType errID = 0)
			: base()
		{
			_joResp = joResp;
			_nErrorType = errID;
		}

		public JObject Response
		{
			get { return _joResp; }
		}

		public JsonRpcResponseErrorType ErrorType { get { return _nErrorType; } }
	}

	public delegate void JonRpcResponseExceptionEvent(JsonRpcExceptionEventArg args);

	public class JsonRpcClient
	{
		TcpClient? _tcpClient;
		readonly Task<TcpClient> _taskConnection;
		NetworkStream? _stream = null;
		readonly ManualResetEvent _evtStreamReady = new ManualResetEvent(false);
		UInt32 _nSeqID = 0;
		readonly object _oSeqID = new ();
		readonly Dictionary<int, RequestState> _dctRequest = new Dictionary<int, RequestState>();

		public bool ConnectedState { get { return _tcpClient != null && _tcpClient.Connected; } }

		public class RequestState
		{
			public int SeqID;
			public bool IsTimeout = false;
			public JObject? Response = null;
			public ManualResetEvent EvtHandle = new ManualResetEvent(false);
		}

		private uint SeqID 
		{
			get
			{
				lock (_oSeqID)
				{
					++_nSeqID;
				}

				return _nSeqID;
			}
		}

		class AsyncState
		{
			public AsyncState(Stream stream, byte[] buffer, MemoryStream ms)
			{
				Stream = stream;
				Buffer = buffer;
				MemoryStream = ms;
			}
			public Stream Stream;
			public byte[] Buffer;
			public MemoryStream MemoryStream;
			public ManualResetEvent EvtHandle = new ManualResetEvent(false);
			public Exception? Exception;
		}

		public event JonRpcResponseExceptionEvent? OnJsonRpcException = null;

		//private JsonRpcClient() { }
		public JsonRpcClient(int port, string ipaddr = "127.0.0.1")
		{
			_stream = null;
			_taskConnection = Task.Run(() =>
			{
				while (true)
				{
					try
					{
						_tcpClient = new TcpClient(ipaddr, port);
						break;
					}
					catch { }

					Thread.Sleep(1000);
				}

				return _tcpClient;
			});
		}

		public void Start()
		{
			_evtStreamReady.Reset();
			_taskConnection.ContinueWith((t) =>
			{
				_stream = t.Result.GetStream();
				_evtStreamReady.Set();

				try
				{
					byte[] buffer = new byte[64 * 1024];
					while (true)
					{
						AsyncState asyncState = new AsyncState(_stream, buffer, new MemoryStream());

						IAsyncResult ar = _stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(JsonRpcAsyncReadCallback), asyncState);

						bool blWaitResult = asyncState.EvtHandle.WaitOne();

						if (blWaitResult == true)
						{
							Task.Run(() =>
							{
								string strResp = Encoding.UTF8.GetString(asyncState.MemoryStream.ToArray());
								Console.WriteLine(strResp);

								JObject? joResp = null;
								try
								{
									joResp = JObject.Parse(strResp);

									if (joResp.TryGetValue("jsonrpc", out JToken? jtJsonRpc)
										&& jtJsonRpc.Type == JTokenType.String
										&& jtJsonRpc.Value<string>() == "2.0"
										&& joResp.TryGetValue("id", out JToken? jtSeqID))
									{
										int nSeqID;
										if (jtSeqID.Type == JTokenType.String)
											nSeqID = int.Parse(jtSeqID.Value<string>()!);
										else if (jtSeqID.Type == JTokenType.Integer)
											nSeqID = jtSeqID.Value<int>();
										else
										{
											Console.WriteLine($"invalid jsonrpc sequeue id: {jtSeqID}");
											DefaultJsonRpcExceptionHandler(joResp, JsonRpcResponseErrorType.InvalidSequenceID);
											return;
										}

										if (_dctRequest.TryGetValue(nSeqID, out RequestState? requestState) && requestState != null)
										{
											requestState.Response = joResp;
											if (requestState.Response.TryGetValue("error", out JToken? jtError))
											{
												Console.WriteLine($"error response: {jtError}");
												DefaultJsonRpcExceptionHandler(joResp, JsonRpcResponseErrorType.ProcessError);
											}
											requestState.EvtHandle.Set();
										}
										else
										{
											Console.WriteLine($"Not found jsonrpc responsed sequence id: {jtSeqID}");
											DefaultJsonRpcExceptionHandler(joResp, JsonRpcResponseErrorType.NotFoundSequenceID);
										}
									}
									else
									{
										Console.WriteLine($"Invalid Jsonrpc response: {joResp}");
										DefaultJsonRpcExceptionHandler(joResp, JsonRpcResponseErrorType.InvalidResponse);
									}
								}
								catch (JsonReaderException ex)
								{
									Console.WriteLine($"Exception: {ex.Message}\n\n{ex.StackTrace}");
									DefaultJsonRpcExceptionHandler(joResp, JsonRpcResponseErrorType.ParseError);
								}
								catch (Exception ex) when (ex is JsonReaderException || ex is ArgumentNullException || ex is FormatException || ex is OverflowException)
								{
									Console.WriteLine($"Exception: {ex.Message}\n\n{ex.StackTrace}");
									DefaultJsonRpcExceptionHandler(joResp, JsonRpcResponseErrorType.InvalidSequenceID);
								}
							});
						}
						else
						{
							Console.WriteLine($"Server {_stream.Socket.RemoteEndPoint} closed");
							break;
						}
					}
				}
				catch (Exception ex)
				{
					Console.WriteLine($"{ex.GetType()}");
					Console.WriteLine($"{ex.Message}");
				}
			});
		}

		private void DefaultJsonRpcExceptionHandler(JObject? joResp, JsonRpcResponseErrorType eErrID)
		{
				OnJsonRpcException?.Invoke(new JsonRpcExceptionEventArg(joResp!, eErrID));
		}

		public void Stop()
		{
			_stream?.Close();
			_tcpClient?.Close();
		}

		private void JsonRpcAsyncReadCallback(IAsyncResult ar)
		{
			var asyncState = (AsyncState)ar.AsyncState!;
			try
			{
				int nBytesRead = asyncState.Stream.EndRead(ar);

				if (nBytesRead == asyncState.Buffer.Length)
				{
					asyncState.MemoryStream.Read(asyncState.Buffer, 0, nBytesRead);
					asyncState.Stream.BeginRead(asyncState.Buffer, 0, nBytesRead, new AsyncCallback(JsonRpcAsyncReadCallback), asyncState);
				}
				else
				{
					asyncState.MemoryStream.Write(asyncState.Buffer, 0, nBytesRead);
					asyncState.EvtHandle.Set();
				}
			}
			catch (IOException ex)
			{
				//Console.WriteLine($"connection: {asyncState.Stream.ToString()} closed");
				//Console.WriteLine($"{ex.Message}");
				asyncState.Exception = ex;
				asyncState.EvtHandle.Set();
			}
			catch (ObjectDisposedException ex)
			{
				//Console.WriteLine($"connection: {asyncState.Stream.ToString()} closed");
				//Console.WriteLine($"{ex.Message}");
				asyncState.Exception = ex;
				asyncState.EvtHandle.Set();
			}
			catch (Exception ex)
			{
				//Console.WriteLine($"connection: {asyncState.Stream.ToString()} closed");
				//Console.WriteLine($"{ex.Message}");
				asyncState.Exception = ex;
				asyncState.EvtHandle.Set();
			}
		}

		public void CallNotifyMethod(string method, JToken jtParams)
		{
			var t = CallMethodAsync(method, jtParams, Timeout.Infinite, true);
			t.ContinueWith((x) => { return true; });
		}

		public async Task<RequestState> CallMethodAsync(string method, JToken jtParams, int nTimeoutMS = Timeout.Infinite, bool blNotify = false)
		{
			JObject joReq = new JObject
			{
				["jsonrpc"] = "2.0",
				["method"] = method,
				["params"] = jtParams
			};
			if (blNotify == false)
				joReq["id"] = SeqID;

			//string strReq = joReq.ToString();
			//byte[] abReq = Encoding.UTF8.GetBytes(strReq);

			return await Task<RequestState>.Run(() =>
			{
				RequestState requestState = new RequestState
				{
					IsTimeout = !_evtStreamReady.WaitOne(nTimeoutMS)
				};

				if (requestState.IsTimeout == true)
				{
					return requestState;
				}

				if (joReq.TryGetValue("id", out JToken? jtSeqID))
				{
					requestState.SeqID = jtSeqID.Value<int>();
					requestState.IsTimeout = false;

					_dctRequest[requestState.SeqID] = requestState;
				}

				string strReq = joReq.ToString(Newtonsoft.Json.Formatting.None);
				byte[] abReq = Encoding.UTF8.GetBytes(strReq);
				while (_stream!.CanWrite == false) Thread.Sleep(1);
				_stream.Write(abReq, 0, abReq.Length);

				if (requestState != null)
				{
					requestState.IsTimeout = !requestState.EvtHandle.WaitOne((int)nTimeoutMS);
				}

				return requestState!;
			});
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="method"></param>
		/// <param name="jtParams"></param>
		/// <param name="nTimeoutMS"></param>
		/// <returns></returns>
		/// <exception cref="TimeoutException"></exception>
		public JObject? CallMehod(string method, JToken jtParams, int nTimeoutMS = Timeout.Infinite)
		{
			JObject joReq = new JObject
			{
				["jsonrpc"] = "2.0",
				["method"] = method,
				["params"] = jtParams,
				["id"] = SeqID
			};

			RequestState requestState = new RequestState
			{
				IsTimeout = !_evtStreamReady.WaitOne(nTimeoutMS)
			};
			if (requestState.IsTimeout == true)
			{
				throw new TimeoutException($"JsonRpc request timeout");
			}

			requestState.SeqID = joReq["id"]!.Value<int>();
			requestState.IsTimeout = false;
			requestState.EvtHandle = new ManualResetEvent(false);

			lock (_dctRequest)
			{
				_dctRequest[requestState.SeqID] = requestState;
			}

			string strReq = joReq.ToString(Newtonsoft.Json.Formatting.None);
			byte[] abReq = Encoding.UTF8.GetBytes(strReq);

			while (_stream!.CanWrite == false) Thread.Sleep(1);
			_stream.Write(abReq, 0, abReq.Length);

			if (requestState != null)
			{
				requestState.IsTimeout = requestState.EvtHandle.WaitOne((int)nTimeoutMS);
			}

			return requestState!.Response;
		}
	}
}
